package mn.more.foundation.io;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import mn.more.foundation.lang.StringUtil;

/**
 * General and useful utility dealing with filesystem. Also contains
 * file-related utilities and properties files handling.
 *
 * @author <a href="mailto:mike.liu@aptechmongolia.edu.mn">Mike Liu</a>
 * @version $Id: FileUtil.java 5 2008-09-01 12:08:42Z mikeliucc $
 */
public final class FileUtil {

	// constants
	private static final String FILE_EXTENSION = ".";

	/** inner class: dir listing filter */
	public static final FileFilter DIR_ONLY_FILTER = new FileFilter() {
		public boolean accept(final File file) { return file.isDirectory(); }
	};

	/** inner class: file extension filter */
	public static final class FileExtentionFilter implements FileFilter {

		private String fileExtension;

		public FileExtentionFilter(String fileExtension) {
			if (StringUtil.isBlank(fileExtension)) {
				throw new IllegalArgumentException("invalid file extension: " +
				                                   fileExtension);
			}

			this.fileExtension = fileExtension;
		}

		public boolean accept(File thisFile) {
			return thisFile.isFile() && thisFile.getName().endsWith(
				fileExtension);
		}
	}

	/** singleon constr. */
	private FileUtil() { }

	/**
	 * append <code>input</code> to the end of the file <code>filename</code>. It
	 * does not append an end-of-line character after that.
	 *
	 * @param filename the name of the file to append.
	 * @param input    the addition text to append to <code>filename</code>.
	 * @throws IOException problem with opening up the file <code>filename</code>.
	 */
	public static void appendToFile(String filename, String input)
		throws IOException {
		// don't bother with useless input
		if (StringUtil.isEmpty(input)) { return; }

		// append to end of file
		// todo: do we need to handle I/O error internally here?
		PrintWriter out =
			new PrintWriter(new BufferedWriter(new FileWriter(filename, true)));
		out.print(input);
		out.close();
	}

	/**
	 * return the content of the text file <code>filename</code>. The file must be
	 * readable or a <code>IOException</code> will be thrown. Successful run of
	 * this function returns a <code>String</code> representation of the content of
	 * <code>filename</code>.
	 *
	 * @param filename full path of the file to be read
	 * @return the content of <code>filename</code>.
	 * @throws IOException in the case where malformated filename is given, or if
	 *                     the file cannot be read
	 */
	public static StringBuffer getContent(String filename) throws IOException {
		// make sure that there is a proper filename
		if (!isFileExists(filename)) {
			throw new IOException(
				"a well-formated full path filename must be specified.");
		}

		BufferedReader br = null;
		try {
			br = new BufferedReader(new FileReader(new File(filename)));
			StringBuffer sbuffer = new StringBuffer();

			// start reading in one line at a time, null represent end of file
			String tmpStr = br.readLine();
			while (tmpStr != null) {
				// append all to the buffer
				sbuffer.append(tmpStr + "\n");
				tmpStr = br.readLine();
			}

			return sbuffer;
		} catch (IOException e) {
			throw e;
		} finally {
			if (br != null) { try { br.close(); } catch (Exception e1) { } }
		}
	}

	/**
	 * return the content of the text file <code>filename</code>. The file must be
	 * readable or a <code>IOException</code> will be thrown. Successful run of
	 * this function returns a <code>String</code> representation of the content of
	 * <code>filename</code>.
	 *
	 * @param resourceName full path of the file to be read
	 * @return the content of <code>filename</code>.
	 * @throws IOException in the case where malformated filename is given, or if
	 *                     the file cannot be read
	 */
	public static StringBuffer getResourceContent(String resourceName)
		throws IOException {
		// make sure that there is a proper filename
		if (StringUtil.isBlank(resourceName)) {
			throw new IOException("invalid resource name: " + resourceName);
		}

		InputStream in = FileUtil.class.getClassLoader().getResourceAsStream(
			resourceName);
		if (in == null) { return null; }

		BufferedReader br = null;
		try {
			br = new BufferedReader(new InputStreamReader(in));
			StringBuffer sbuffer = new StringBuffer();

			// start reading in one line at a time, null represent end of file
			String tmpStr = br.readLine();
			while (tmpStr != null) {
				// append all to the buffer
				sbuffer.append(tmpStr + "\n");
				tmpStr = br.readLine();
			}

			return sbuffer;
		} catch (IOException e) {
			throw e;
		} finally {
			if (br != null) { try { br.close(); } catch (Exception e1) { } }
			try { in.close(); } catch (IOException e) { }
		}
	}

	/**
	 * return a <code>String</code> object from the <code>reader</code> read from
	 * its current position up to the first occurence of <code>stopChar</code>.
	 *
	 * @param reader   java.io.Reader
	 * @param stopChar char
	 * @return java.lang.String
	 * @see #readUntil(java.io.FileInputStream,char)
	 */
	public static String readUntil(Reader reader, char stopChar)
		throws IOException {
		// sanity check
		if (reader == null) { return null; }

		BufferedReader bufferedReader = reader instanceof BufferedReader ?
		                                (BufferedReader) reader :
		                                new BufferedReader(reader);
		int i = bufferedReader.read();
		if (i == -1) {
			throw new EOFException("End of file reached unexpectedly.");
		}

		char ch = (char) i;

		StringBuilder buffer = new StringBuilder();

		// read until the stopChar is encountered
		while (ch != stopChar) {
			buffer.append(ch);

			i = bufferedReader.read();
			if (i == -1) {
				throw new EOFException("End of file reached unexpectedly.");
			}
			ch = (char) i;
		}

		return buffer.toString();
	}

	/**
	 * return a <code>String</code> object from the <code>instream</code> read from
	 * its current position up to the first occurence of <code>stopChar</code>.
	 *
	 * @param stopChar char
	 * @return java.lang.String
	 * @see #readUntil(Reader,char)
	 */
	public static String readUntil(FileInputStream instream, char stopChar)
		throws IOException {
		// sanity check
		if (instream == null) { return null; }
		return readUntil(new InputStreamReader(instream), stopChar);
	}

	/**
	 * write <code>input<code> to the file name <code>filename</code>.
	 * <code>IOException</code> is thrown when the file cannot be access or the
	 * file cannot be written.
	 *
	 * @param filename java.lang.String
	 * @param input    java.lang.String
	 */
	public static void writeTextToFile(String filename, Object input)
		throws IOException {
		// sanity check
		if (StringUtil.isBlank(filename)) { return; }
		if (input == null) { return; }

		// create the directory if needed
		FileOperator.createDirectory((new File(filename)).getParent());

		// write to file
		PrintWriter out = null;
		try {
			out = new PrintWriter(new BufferedWriter(new FileWriter(filename)));
			out.print(input);
			out.flush();
		} catch (IOException e) {
			throw e;
		} finally {
			if (out != null) { out.close(); }
		}
	}

	/** write <code>binaryData</code> to <code>filename</code> as binary stream. */
	public static void writeBinaryToFile(String filename, Object binaryData)
		throws IOException {
		// sanity check
		if (StringUtil.isBlank(filename)) { return; }
		if (binaryData == null) { return; }

		// make sure that all neccessary directories are created
		FileOperator.createDirectory((new File(filename)).getParent());

		// persist file as binary stream
		ObjectOutputStream out = null;
		try {
			out = new ObjectOutputStream(new FileOutputStream(filename));
			out.writeObject(binaryData);
		} catch (IOException e) {
			// delete this file; it's unusable
			FileOperator.delete(filename);
			throw e;
		} finally {
			if (out != null) { try { out.close(); } catch (IOException e) { } }
		}
	}

	/** read <code>binaryData</code> from <code>filename</code> as binary stream. */
	public static Object readBinaryFromFile(String filename)
		throws IOException {
		// sanity check
		if (StringUtil.isBlank(filename)) { return null; }

		// read file as binary stream
		ObjectInputStream in = null;
		try {
			in = new ObjectInputStream(new FileInputStream(filename));
			return in.readObject();
		} catch (IOException e) {
			throw e;
		} catch (ClassNotFoundException e) {
			throw new IOException("Unrecognized class in object stream: " +
			                      e.getMessage());
		} finally {
			if (in != null) { try { in.close(); } catch (IOException e) { } }
		}
	}

	/**
	 * returns a file reader (<code>java.io.Reader</code>) of the file specified by
	 * the <code>fileLocation</code>.  This reader can then be used by the calling
	 * class to read the contents of the file specified.
	 *
	 * @return java.io.FileInputStream
	 * @throws IOException The exception description.
	 * @see java.io.BufferedReader
	 * @see Reader
	 */
	public static Reader getReader(String fileLocation) throws IOException {
		// make sure that there is a proper filename
		if (!isFileExists(fileLocation)) {
			// todo:ResourceBundle for internationalization
			throw new IOException(
				"a well-formated full path filename must be specified.");
		}

		// try to use the file
		return new BufferedReader(new FileReader(new File(fileLocation)));
	}

	/**
	 * returns a string of size <code>length</code> from <code>reader</code>.
	 *
	 * @param length int
	 * @return java.lang.String
	 */
	public static String getString(Reader reader, int length)
		throws IOException {
		// sanity check
		if (reader == null) { return null; }
		if (length < 1) {
			throw new IllegalArgumentException("length " +
			                                   length +
			                                   " must be greater than 0");
		}

		StringBuilder buffer = new StringBuilder();
		BufferedReader bufferedReader = reader instanceof BufferedReader ?
		                                (BufferedReader) reader :
		                                new BufferedReader(reader);

		// go through as many char as length specified
		while (buffer.length() < length) {
			char ch = (char) bufferedReader.read();
			if (ch == '\uFFFF') {
				return "";
			} else {
				buffer.append(ch);
			}
		}

		// done
		return buffer.toString();
	}

	/**
	 * return the content of the text file <code>filename</code>. The file must be
	 * readable or a <code>java.io.IOException</code> will be thrown. Successful
	 * run of this function returns a <code>java.util.List</code> representation of
	 * the content of <code>filename</code> with every element in the vector
	 * containing a line of text from <code>filename</code>.
	 *
	 * @param filename java.lang.String full path of the file to be read
	 * @return String the content of <code>filename</code>.
	 * @throws IOException <<Invalid>> in the case where malformated filename is
	 *                     given, or if the file cannot be read
	 */
	public static List getText(String filename) throws IOException {
		// make sure that there is a proper filename
		if (filename == null || filename.length() < 1) {
			// todo: resource bundle for internationalization
			throw new IOException(
				"a well-formated full path filename must be specified.");
		}

		File file = new File(filename);
		return getText(file);
	}

	/**
	 * Same as <code>getText(String)</code>.  This method takes <code>File</code>
	 * object instead of filename.  See <code>getText</code> method for more
	 * details.
	 *
	 * @param file java.io.File object to be read
	 * @return String the content of <code>file</code>.
	 * @see #getText(java.lang.String)
	 */
	public static List getText(File file) throws IOException {
		if (file == null) { return null; }
		if (!file.exists() ||
		    !file.canRead() ||
		    !file.isFile() ||
		    file.length() == 0) {
			throw new IOException("Cannot read file " + file.getAbsoluteFile());
		}

		// used to be synchronized
		List list = new ArrayList();

		// try to use the file
		BufferedReader br = null;

		try {
			br = new BufferedReader(new FileReader(file));

			// start reading in one line at a time, null represent end of file
			String tmpStr = br.readLine();
			while (tmpStr != null) {
				list.add(tmpStr);
				tmpStr = br.readLine();
			}

			return list;
			/* (mike, 07/30/2003 04:51:42): not necessary */
			//} catch (IOException e) {
			//	throw e;
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
					// don't worry about it.
					//logger.debug("[NO BIG DEAL] error closing reader for " + file + ": " + e.getMessage());
				}
			}
		}
	}

	/**
	 * return the file extension (without the extension delimeter itself) of
	 * <code>filename</code>. This is just a convenience method, no magic here.
	 *
	 * @param filename java.lang.String
	 * @return java.lang.String
	 */
	public static String getFileExtension(String filename) {
		// sanity check
		if (StringUtil.isBlank(filename)) { return null; }
		int extensionPosition = filename.lastIndexOf(FILE_EXTENSION);
		if (extensionPosition == -1) { return null; }// no dot
		if (extensionPosition == (filename.length() - 1)) {
			return null;
		}// dot is last char, not very possible
		if (extensionPosition == 0) { return null; }// dot is the first char
		return filename.substring(extensionPosition + 1);
	}

	/**
	 * return the name of the file specified in the <code>fullPath</code>. If
	 * <code>withExtension</code> is specified, the returning filename will include
	 * the extension as well.
	 *
	 * @param withExtension determine if the filename should be returned as well.
	 * @return filename
	 */
	public static String getFilename(String fullPath, boolean withExtension) {
		// sanity check
		if (StringUtil.isBlank(fullPath)) { return null; }

		// get filename
		int index = fullPath.lastIndexOf(File.separator);
		if (index < 0 && File.separatorChar != '/') {
			// eager search -- for platform/shell compatibility
			index = fullPath.lastIndexOf("/");
		}

		int extensionIndex = fullPath.lastIndexOf(".");

		// extension or no extension
		if (withExtension || extensionIndex < 0) {
			return fullPath.substring(index + 1);
		} else {
			return fullPath.substring(index + 1, extensionIndex);
		}
	}

	/**
	 * get directory name of a full path (which excludes the file name)
	 *
	 * @return full path of a file (excluding file name)
	 */
	public static String getPath(String fullPath) {
		// sanity check
		if (StringUtil.isBlank(fullPath)) { return null; }

		// UNIX-based file system
		if (File.separatorChar == '/') {
			File file = new File(fullPath);
			return file.getParent();
		}

		// Win32 crap...
		// since we can't be certain, we'll try to use both slashes
		int lastSlash = fullPath.lastIndexOf("/");
		int lastSlash2 = fullPath.lastIndexOf("\\");
		return fullPath.substring(0, Math.max(lastSlash, lastSlash2));
	}

	/**
	 * returns the last sub-directory name in <code>fullPath</code>.
	 *
	 * @return name of current directory for the file resides in
	 */
	public static String getCurrentPath(String fullPath) {
		// sanity check
		if (StringUtil.isBlank(fullPath)) { return null; }

		// UNIX-based file system
		if (File.separatorChar == '/') {
			File file = new File(fullPath);
			return file.getParentFile().getName();
		}

		// Win32 crap...

		// get full path name
		String fullPathName = FileUtil.getPath(fullPath);
		if (StringUtil.isBlank(fullPathName)) { return null; }

		// since we can't be certain, we'll try to use both slashes
		int lastSlash = fullPathName.lastIndexOf("/");
		int lastSlash2 = fullPathName.lastIndexOf("\\");
		return fullPathName.substring(Math.max(lastSlash, lastSlash2) + 1);
	}

	/**
	 * determine if <code>directory</code> is indeed a directory
	 *
	 * @return boolean
	 */
	public static boolean isDirectory(String directory) {
		// sanity check
		return !StringUtil.isBlank(directory) &&
		       new File(directory).isDirectory();
	}

	/**
	 * Determine if a file exists and readable.
	 *
	 * @return boolean
	 */
	public static boolean isFileExists(String filename) {
		// sanity check
		if (StringUtil.isBlank(filename)) { return false; }
		File file = new File(filename);
		return file.isFile() && file.canRead();
	}
}
